/**!
web module

*/
use askama::Template;
use axum::{
    body::Body,
    error_handling::HandleErrorLayer,
    extract,
    extract::{Query, State},
    http::Uri,
    http::{Request, StatusCode},
    response::IntoResponse,
    response::{Html, Json, Response},
    routing::get,
    Router,
};
use common::{self, SearchDoc};
use serde::{ Deserialize, Serialize};
use std::{borrow::Cow,net::SocketAddr, sync::Arc, time::Duration};
use tokio::signal;
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::{services::ServeFile, trace::TraceLayer};
use tantivy::doc;

#[cfg(feature = "tracelog")]
use tracing::{debug, error, info};
#[cfg(feature = "rotatelog")]
use log::{debug, error, info, warn};

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Params {
    //  #[serde(default, deserialize_with = "empty_string_as_none")]
    page: Option<i32>,
    q: Option<String>,
    qtype: Option<String>,
    file: Option<bool>,
    author: Option<bool>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ReadParams {
    //  #[serde(default, deserialize_with = "empty_string_as_none")]
    fname: Option<String>,
}

type SharedNss = Arc<search::Nss>;

pub async fn beijing_main(state: SharedNss) -> Result<(), common::Error> {
    // initialize tracing
    let app = Router::with_state(Arc::clone(&state))
        .route("/", get(root))
        .route("/s", get(search_handler))
        .route("/api/s", get(api_search_handler))
        .route("/greet/:name", get(greet))
        .route("/f", get(doc_file_read))
        .layer(
            ServiceBuilder::new()
                // Handle errors from middleware
                .layer(HandleErrorLayer::new(handle_error))
                .load_shed()
                .concurrency_limit(1024)
                .timeout(Duration::from_secs(10))
                .layer(TraceLayer::new_for_http())
                .into_inner(),
        )
        .fallback(fallback);

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
    tracing::debug!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();

    Ok(())
}

async fn fallback(uri: Uri) -> impl IntoResponse {
    (StatusCode::NOT_FOUND, format!("No route for uri {}", uri))
}

async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    println!("signal received, starting graceful shutdown");
}

// basic handler that responds with a static string
async fn root(Query(args): Query<Params>) -> impl IntoResponse {
    let qry = args.q.unwrap_or("".to_string());
    let page = args.page.unwrap_or(1);
    let rets = SearchResult {
        qry,
        page,
        info: "".to_string(),
        results: vec![],
    };
    HtmlTemplate(rets)
}

async fn search_handler(
    State(state): State<SharedNss>,
    Query(args): Query<Params>,
) -> impl IntoResponse {
    let qry = args.q.unwrap_or("".to_string());
    let qtype = args.qtype.unwrap_or("".to_string());
    let page = args.page.unwrap_or(1);

    let retd = search::do_search(
        Arc::clone(&state),
        qry.as_str(),
        qtype.as_str(),
        page,
        false,
    )
    .await;
    let mut rets = SearchResult {
        qry: qry.clone(),
        info: "".to_string(),
        page,
        results: vec![],
    };
    match retd {
        Ok(docs) => {
            rets = SearchResult {
                qry,
                page,
                info: "".to_string(),
                results: docs,
            };
        }
        Err(err) => {
            let einfo = format!("{:#?}", err);
            rets.info = einfo;
            error!(rets.info);
        }
    }
    HtmlTemplate(rets)
}

async fn api_search_handler(
    State(state): State<SharedNss>,
    Query(args): Query<Params>,
) -> impl IntoResponse {
    let qry = args.q.unwrap_or("".to_string());
    let qtype = args.qtype.unwrap_or("".to_string());
    let page = args.page.unwrap_or(1);

    let retd =
        search::do_search(Arc::clone(&state), qry.as_str(), qtype.as_str(), page, true).await;
    match retd {
        Ok(docs) => {
            let rets = SearchResult {
                qry,
                page,
                info: "".to_string(),
                results: docs,
            };
            (StatusCode::OK, Json(rets))
        }
        Err(err) => {
            let einfo = format!("{:#?}", err);
            error!(einfo);
            let rets = SearchResult {
                qry,
                page,
                info: "".to_string(),
                results: vec![],
            };
            (StatusCode::OK, Json(rets))
        }
    }
}

async fn greet(extract::Path(name): extract::Path<String>) -> impl IntoResponse {
    let para = Hello { name };

    HtmlTemplate(para)
}

async fn doc_file_read(Query(read_par): Query<ReadParams>) -> impl IntoResponse {
    debug!(?read_par);
    let filename = read_par.fname.unwrap();
    let svc = ServeFile::new_with_mime(filename, &mime::TEXT_PLAIN_UTF_8);
    // let svc = ServeFile::new( fname);
    let mut request = Request::new(Body::empty());
    let res = svc.oneshot(request).await.unwrap();
    res
    /*
    let mut content = String::new();
    let path = Path::new(filename.as_str());
    // 以只读方式打开路径，返回 `io::Result<File>`
    File::open(&path).unwrap().read_to_string(&mut content).unwrap();
    (StatusCode::OK,  Cow::from( content )) */
}

async fn handle_error(error: BoxError) -> impl IntoResponse {
    if error.is::<tower::timeout::error::Elapsed>() {
        return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out"));
    }

    if error.is::<tower::load_shed::error::Overloaded>() {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Cow::from("service is overloaded, try again later"),
        );
    }

    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Cow::from(format!("Unhandled internal error: {}", error)),
    )
}

#[derive(Template)]
#[template(path = "hello.html")]
struct Hello {
    name: String,
}

struct HtmlTemplate<T>(T);
impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    fn into_response(self) -> Response {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Failed to render template. Error: {}", err),
            )
                .into_response(),
        }
    }
}

#[derive(Template, Default, Serialize, Deserialize, Debug)]
#[template(path = "search.html")]
pub struct SearchResult {
    pub qry: String,
    pub info: String,
    pub page: i32,
    pub results: Vec<common::SearchDoc>,
}
